/*
 *  Copyright (c) 2011 Leibniz Institute of Plant Genetics and Crop Plant Research (IPK), Gatersleben, Germany.
 *  All rights reserved. This program and the accompanying materials
 *  are made available under the terms of the GNU Lesser Public License v2.1
 *  which accompanies this distribution, and is available at
 *  http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
 *
 *  Contributors:
 *      Leibniz Institute of Plant Genetics and Crop Plant Research (IPK), Gatersleben, Germany - initial API and implementation
 */
package de.ipk_gatersleben.bit.bi.edal.primary_data.file.implementation;

import java.io.Serializable;
import java.security.Principal;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;

import org.hibernate.Criteria;
import org.hibernate.Session;
import org.hibernate.Transaction;
import org.hibernate.criterion.Restrictions;

import de.ipk_gatersleben.bit.bi.edal.aspectj.security.GrantableMethods;
import de.ipk_gatersleben.bit.bi.edal.aspectj.security.GrantableMethods.EdalClasses;
import de.ipk_gatersleben.bit.bi.edal.aspectj.security.GrantableMethods.Methods;
import de.ipk_gatersleben.bit.bi.edal.primary_data.DataManager;
import de.ipk_gatersleben.bit.bi.edal.primary_data.file.PrimaryDataEntity;
import de.ipk_gatersleben.bit.bi.edal.primary_data.file.PrimaryDataEntityException;
import de.ipk_gatersleben.bit.bi.edal.primary_data.security.EdalPermission;
import de.ipk_gatersleben.bit.bi.edal.primary_data.security.PermissionProvider;

/**
 * Implementation of {@link PermissionProvider} interface.
 * 
 * @author arendd
 */
public class PermissionProviderImplementation implements Serializable,
		PermissionProvider {

	private static final String CACHE_REGION_ROOT = "query.root";
	private static final String CACHE_REGION_FOR_PRINCIPALS = "query.principal";
	private static final String CACHE_REGION_FOR_PERMISSIONS = "query.permission";
	private static final String STRING_INTERN_VERSION = "internVersion";
	private static final String STRING_INTERN_METHOD = "internMethod";
	private static final String STRING_INTERN_CLASS = "internClass";
	private static final String STRING_INTERN_ID = "internId";
	private static final String STRING_PRINCIPAL = "principal";
	private static final String STRING_TYPE = "type";
	private static final String STRING_NAME = "name";

	private static final long serialVersionUID = -8397868034521482885L;
	/**
	 * store the currentEntity ID for database Query in findPermissionsFromDB
	 */

	private static final InheritableThreadLocal<String> THREAD_LOCAL_ENTITY_ID;

	static {
		THREAD_LOCAL_ENTITY_ID = new InheritableThreadLocal<String>();
	}

	/**
	 * Getter for the field <code>THREAD_LOCAL_ENTITY_ID</code>.
	 * 
	 * @return the THREAD_LOCAL_ENTITY_ID
	 */
	public static InheritableThreadLocal<String> getThreadlocalentityid() {
		return THREAD_LOCAL_ENTITY_ID;
	}

	/** {@inheritDoc} */
	@Override
	public List<EdalPermission> findPermissions(
			final Set<Principal> principalList) {

		DataManager.getImplProv().getLogger().debug("Start FindPermission ");

		final List<EdalPermission> permissions = new ArrayList<EdalPermission>();

		List<EdalPermissionImplementation> internalPermissions = null;

		for (Principal principal : principalList) {
			try {
				internalPermissions = getEDALPermissionsFromDB(principal
						.getClass().getSimpleName(), principal.getName());
				for (EdalPermissionImplementation edalperm : internalPermissions) {
					permissions.add(edalperm.toEdalPermission());
				}
			} finally {
				// break when found one correct principal
				if (!internalPermissions.isEmpty()) {
					break;
				}
			}
		}

		/*
		 * if the user subject consists no granted principal, then check if the
		 * ALLPrincipal is set
		 */
		if (permissions.isEmpty()) {

			internalPermissions = getEDALPermissionsFromDB(
					ALLPrincipal.class.getSimpleName(),
					new ALLPrincipal().getName());

			for (EdalPermissionImplementation edalperm : internalPermissions) {
				permissions.add(edalperm.toEdalPermission());
			}
		}

		return permissions;
	}

	/**
	 * Query all permission for the current principal
	 * 
	 * @param principalType
	 *            the type of the {@link Principal}
	 * @param principalName
	 *            the name of the {@link Principal}
	 * @return list of all {@link EdalPermission}
	 */
	private List<EdalPermissionImplementation> getEDALPermissionsFromDB(
			final String principalType, final String principalName) {

		Session session = ((FileSystemImplementationProvider) DataManager
				.getImplProv()).getSession();

		Criteria principalQuery = session
				.createCriteria(PrincipalImplementation.class)
				.add(Restrictions.eq(STRING_NAME, principalName))
				.add(Restrictions.eq(STRING_TYPE, principalType))
				.setCacheable(true).setCacheRegion(CACHE_REGION_FOR_PRINCIPALS);

		PrincipalImplementation principal = (PrincipalImplementation) principalQuery
				.uniqueResult();

		Criteria permissionQuery = session
				.createCriteria(EdalPermissionImplementation.class)
				.add(Restrictions.eq(STRING_INTERN_ID, getThreadlocalentityid()
						.get()))
				.add(Restrictions.eq(STRING_PRINCIPAL, principal))
				.setCacheable(true)
				.setCacheRegion(CACHE_REGION_FOR_PERMISSIONS);

		@SuppressWarnings("unchecked")
		List<EdalPermissionImplementation> permissions = (List<EdalPermissionImplementation>) permissionQuery
				.list();
		session.close();

		return permissions;

	}

	/**
	 * {@inheritDoc}
	 * <p>
	 * Check if the {@link Principal} or the {@link EdalPermission} exists
	 * before grant new permission.
	 */
	@Override
	public void grantPermission(final String principalType,
			final String principalName, final EdalPermission edalPermission)
			throws PrimaryDataEntityException {

		Session session = ((FileSystemImplementationProvider) DataManager
				.getImplProv()).getSession();

		PrincipalImplementation principal = (PrincipalImplementation) session
				.createCriteria(PrincipalImplementation.class)
				.add(Restrictions.eq(STRING_NAME, principalName))
				.add(Restrictions.eq(STRING_TYPE, principalType))
				.setCacheable(true).setCacheRegion(CACHE_REGION_FOR_PRINCIPALS)
				.uniqueResult();

		// check if principal exists
		if (principal == null) {

			principal = new PrincipalImplementation(principalName,
					principalType);

			EdalPermissionImplementation newPermission = new EdalPermissionImplementation(
					principal, edalPermission.getPrimaryDataEntityID(),
					edalPermission.getVersion(),
					EdalClasses.valueOf(edalPermission.getActionClass()
							.getSimpleName()), Methods.valueOf(edalPermission
							.getActionMethod().getName()));

			Transaction transaction = null;
			try {
				transaction = session.beginTransaction();
				// if principal not exists, save new principal
				session.save(principal);
				// save new permission for new principal
				session.save(newPermission);

				transaction.commit();
				session.close();
			} catch (Exception e) {
				if (transaction != null) {
					transaction.rollback();
					throw new PrimaryDataEntityException(
							"Can not save principal for permission: "
									+ e.getMessage() + "-> rollback");
				}
				throw new PrimaryDataEntityException(
						"Can not save principal for permission: "
								+ e.getMessage());
			}
		}

		else {
			// check if permissions exists
			EdalPermissionImplementation permission = (EdalPermissionImplementation) session
					.createCriteria(EdalPermissionImplementation.class)
					.add(Restrictions.eq(STRING_INTERN_ID,
							edalPermission.getPrimaryDataEntityID()))
					.add(Restrictions.eq(STRING_PRINCIPAL, principal))
					.add(Restrictions.eq(STRING_INTERN_CLASS, EdalClasses
							.valueOf(edalPermission.getActionClass()
									.getSimpleName())))
					.add(Restrictions.eq(STRING_INTERN_METHOD,
							Methods.valueOf(edalPermission.getActionMethod()
									.getName())))
					.add(Restrictions.eq(STRING_INTERN_VERSION,
							edalPermission.getVersion())).setCacheable(true)
					.setCacheRegion(CACHE_REGION_FOR_PERMISSIONS)
					.uniqueResult();

			if (permission == null) {
				EdalPermissionImplementation newPermission = new EdalPermissionImplementation(
						principal, edalPermission.getPrimaryDataEntityID(),
						edalPermission.getVersion(),
						EdalClasses.valueOf(edalPermission.getActionClass()
								.getSimpleName()),
						Methods.valueOf(edalPermission.getActionMethod()
								.getName()));

				Transaction transaction2 = null;
				try {
					transaction2 = session.beginTransaction();
					// save new permission for new principal
					session.save(newPermission);
					transaction2.commit();
				} catch (Exception e) {
					if (transaction2 != null) {
						transaction2.rollback();
						throw new PrimaryDataEntityException(
								"Can not save permission: " + e.getMessage()
										+ "-> rollback");
					}
					throw new PrimaryDataEntityException(
							"Can not save permission: " + e.getMessage());
				}

			}
			session.close();
		}

	}

	/** {@inheritDoc} */
	@Override
	public void grantPermission(String principalType, String principalName,
			PrimaryDataEntity entity) throws PrimaryDataEntityException {

		Session session = ((FileSystemImplementationProvider) DataManager
				.getImplProv()).getSession();

		PrincipalImplementation principal = (PrincipalImplementation) session
				.createCriteria(PrincipalImplementation.class)
				.add(Restrictions.eq(STRING_NAME, principalName))
				.add(Restrictions.eq(STRING_TYPE, principalType))
				.setCacheable(true).setCacheRegion(CACHE_REGION_FOR_PRINCIPALS)
				.uniqueResult();

		// check if principal exists
		if (principal == null) {

			principal = new PrincipalImplementation(principalName,
					principalType);

			Transaction transaction = session.beginTransaction();
			// if principal not exists, save new principal
			session.save(principal);
			transaction.commit();

		}

		/* check if the principal is the ALLPrincipal */

		Boolean isAllPrincipal = false;
		if (principal.getType().equals(ALLPrincipal.class.getSimpleName())
				&& principal.getName().equals(new ALLPrincipal().getName())) {
			isAllPrincipal = true;
		}

		Transaction trans = session.beginTransaction();

		for (final Methods method : GrantableMethods.ENTITY_METHODS) {

			/* if ALLPrincipal, then set no permission for grant/revoke */

			if (!(isAllPrincipal && (method.equals(Methods.grantPermission) || method
					.equals(Methods.revokePermission)))) {

				EdalPermissionImplementation permission = (EdalPermissionImplementation) session
						.createCriteria(EdalPermissionImplementation.class)
						.add(Restrictions.eq(STRING_INTERN_ID, entity.getID()))
						.add(Restrictions.eq(STRING_PRINCIPAL, principal))
						.add(Restrictions.eq(STRING_INTERN_CLASS,
								EdalClasses.PrimaryDataEntity))
						.add(Restrictions.eq(STRING_INTERN_METHOD, method))
						.add(Restrictions.eq(STRING_INTERN_VERSION, entity
								.getCurrentVersion().getRevision()))
						.setCacheable(true)
						.setCacheRegion(CACHE_REGION_FOR_PERMISSIONS)
						.uniqueResult();

				// check if permission exists
				if (permission == null) {
					EdalPermissionImplementation newPermission = new EdalPermissionImplementation(
							principal, entity.getID(), entity
									.getCurrentVersion().getRevision(),
							EdalClasses.PrimaryDataEntity, method);

					// if permission not exists, save new permission
					session.save(newPermission);
				}
			}

		}

		if (entity.isDirectory()) {
			for (final Methods method : GrantableMethods.DIRECTORY_METHODS) {
				EdalPermissionImplementation permission = (EdalPermissionImplementation) session
						.createCriteria(EdalPermissionImplementation.class)
						.add(Restrictions.eq(STRING_INTERN_ID, entity.getID()))
						.add(Restrictions.eq(STRING_PRINCIPAL, principal))
						.add(Restrictions.eq(STRING_INTERN_CLASS,
								EdalClasses.PrimaryDataDirectory))
						.add(Restrictions.eq(STRING_INTERN_METHOD, method))
						.add(Restrictions.eq(STRING_INTERN_VERSION, entity
								.getCurrentVersion().getRevision()))
						.setCacheable(true)
						.setCacheRegion(CACHE_REGION_FOR_PERMISSIONS)
						.uniqueResult();

				// check if permission exists
				if (permission == null) {
					EdalPermissionImplementation newPermission = new EdalPermissionImplementation(
							principal, entity.getID(), entity
									.getCurrentVersion().getRevision(),
							EdalClasses.PrimaryDataDirectory, method);

					// if permission not exists, save new permission
					session.save(newPermission);
				}
			}
		}

		if (!entity.isDirectory()) {
			for (final Methods method : GrantableMethods.FILE_METHODS) {
				EdalPermissionImplementation permission = (EdalPermissionImplementation) session
						.createCriteria(EdalPermissionImplementation.class)
						.add(Restrictions.eq(STRING_INTERN_ID, entity.getID()))
						.add(Restrictions.eq(STRING_PRINCIPAL, principal))
						.add(Restrictions.eq(STRING_INTERN_CLASS,
								EdalClasses.PrimaryDataFile))
						.add(Restrictions.eq(STRING_INTERN_METHOD, method))
						.add(Restrictions.eq(STRING_INTERN_VERSION, entity
								.getCurrentVersion().getRevision()))
						.setCacheable(true)
						.setCacheRegion(CACHE_REGION_FOR_PERMISSIONS)
						.uniqueResult();

				// check if permission exists
				if (permission == null) {
					EdalPermissionImplementation newPermission = new EdalPermissionImplementation(
							principal, entity.getID(), entity
									.getCurrentVersion().getRevision(),
							EdalClasses.PrimaryDataFile, method);

					// if permission not exists, save new permission
					session.save(newPermission);
				}
			}
		}

		trans.commit();

		session.close();
	}

	/** {@inheritDoc} */
	@Override
	public boolean isRoot(final Principal principal) {

		Session session = ((FileSystemImplementationProvider) DataManager
				.getImplProv()).getSession();

		RootImplementation root = (RootImplementation) session
				.createCriteria(RootImplementation.class).setCacheable(true)
				.setCacheRegion(CACHE_REGION_ROOT).uniqueResult();

		if (root == null) {
			Transaction transaction = session.beginTransaction();
			session.save(new RootImplementation(principal.getName(), principal
					.getClass().getSimpleName()));
			transaction.commit();
			session.close();
			return true;
		} else {
			if (root.getName().equals(principal.getName())
					&& root.getType().equals(
							principal.getClass().getSimpleName())) {
				session.close();
				return true;
			} else {
				session.close();
				return false;
			}
		}
	}

	/** {@inheritDoc} */
	@Override
	public void revokePermission(final String principalType,
			final String principalName, final EdalPermission edalPermission)
			throws PrimaryDataEntityException {

		Session session = ((FileSystemImplementationProvider) DataManager
				.getImplProv()).getSession();

		PrincipalImplementation principal = (PrincipalImplementation) session
				.createCriteria(PrincipalImplementation.class)
				.add(Restrictions.eq(STRING_NAME, principalName))
				.add(Restrictions.eq(STRING_TYPE, principalType))
				.setCacheable(true).setCacheRegion(CACHE_REGION_FOR_PRINCIPALS)
				.uniqueResult();

		if (principal == null) {
			throw new PrimaryDataEntityException(
					"couldn't found the correct principal to delete permission"
							+ edalPermission.getActionMethod().getName());
		}

		EdalPermissionImplementation permission = (EdalPermissionImplementation) session
				.createCriteria(EdalPermissionImplementation.class)
				.add(Restrictions.eq(STRING_INTERN_CLASS, EdalClasses
						.valueOf(edalPermission.getActionClass()
								.getSimpleName())))
				.add(Restrictions.eq(STRING_INTERN_ID,
						edalPermission.getPrimaryDataEntityID()))
				.add(Restrictions.eq(STRING_INTERN_METHOD, Methods
						.valueOf(edalPermission.getActionMethod().getName())))
				.add(Restrictions.eq(STRING_INTERN_VERSION,
						edalPermission.getVersion()))
				.add(Restrictions.eq(STRING_PRINCIPAL, principal))
				.setCacheable(true)
				.setCacheRegion(CACHE_REGION_FOR_PERMISSIONS).uniqueResult();

		if (permission == null) {
			throw new PrimaryDataEntityException(
					"couldn't found method permission to delete "
							+ edalPermission.getActionMethod().getName());
		}
		Transaction transaction = session.beginTransaction();
		// delete Permission
		session.delete(permission);
		transaction.commit();
		session.close();

	}

	/** {@inheritDoc} */
	@Override
	public void revokePermission(String principalType, String principalName,
			PrimaryDataEntity entity) throws PrimaryDataEntityException {

		Session session = ((FileSystemImplementationProvider) DataManager
				.getImplProv()).getSession();

		PrincipalImplementation principal = (PrincipalImplementation) session
				.createCriteria(PrincipalImplementation.class)
				.add(Restrictions.eq(STRING_NAME, principalName))
				.add(Restrictions.eq(STRING_TYPE, principalType))
				.setCacheable(true).setCacheRegion(CACHE_REGION_FOR_PRINCIPALS)
				.uniqueResult();

		if (principal == null) {
			throw new PrimaryDataEntityException(
					"couldn't found the correct principal to delete permission");
		}

		for (final Methods method : GrantableMethods.ENTITY_METHODS) {

			EdalPermissionImplementation permission = (EdalPermissionImplementation) session
					.createCriteria(EdalPermissionImplementation.class)
					.add(Restrictions.eq(STRING_INTERN_ID, entity.getID()))
					.add(Restrictions.eq(STRING_PRINCIPAL, principal))
					.add(Restrictions.eq(STRING_INTERN_CLASS,
							EdalClasses.PrimaryDataEntity))
					.add(Restrictions.eq(STRING_INTERN_METHOD, method))
					.add(Restrictions.eq(STRING_INTERN_VERSION, entity
							.getCurrentVersion().getRevision()))
					.setCacheable(true)
					.setCacheRegion(CACHE_REGION_FOR_PERMISSIONS)
					.uniqueResult();

			// if permission exists -> delete
			if (permission != null) {
				Transaction transaction = session.beginTransaction();
				session.delete(permission);
				transaction.commit();
			}

		}

		if (entity.isDirectory()) {
			for (final Methods method : GrantableMethods.DIRECTORY_METHODS) {
				EdalPermissionImplementation permission = (EdalPermissionImplementation) session
						.createCriteria(EdalPermissionImplementation.class)
						.add(Restrictions.eq(STRING_INTERN_ID, entity.getID()))
						.add(Restrictions.eq(STRING_PRINCIPAL, principal))
						.add(Restrictions.eq(STRING_INTERN_CLASS,
								EdalClasses.PrimaryDataDirectory))
						.add(Restrictions.eq(STRING_INTERN_METHOD, method))
						.add(Restrictions.eq(STRING_INTERN_VERSION, entity
								.getCurrentVersion().getRevision()))
						.setCacheable(true)
						.setCacheRegion(CACHE_REGION_FOR_PERMISSIONS)
						.uniqueResult();

				// if permission exists -> delete
				if (permission != null) {
					Transaction transaction = session.beginTransaction();
					session.delete(permission);
					transaction.commit();
				}
			}
		}

		if (!entity.isDirectory()) {
			for (final Methods method : GrantableMethods.FILE_METHODS) {
				EdalPermissionImplementation permission = (EdalPermissionImplementation) session
						.createCriteria(EdalPermissionImplementation.class)
						.add(Restrictions.eq(STRING_INTERN_ID, entity.getID()))
						.add(Restrictions.eq(STRING_PRINCIPAL, principal))
						.add(Restrictions.eq(STRING_INTERN_CLASS,
								EdalClasses.PrimaryDataFile))
						.add(Restrictions.eq(STRING_INTERN_METHOD, method))
						.add(Restrictions.eq(STRING_INTERN_VERSION, entity
								.getCurrentVersion().getRevision()))
						.setCacheable(true)
						.setCacheRegion(CACHE_REGION_FOR_PERMISSIONS)
						.uniqueResult();

				// if permission exists -> delete
				if (permission != null) {
					Transaction transaction = session.beginTransaction();
					session.delete(permission);
					transaction.commit();
				}
			}
		}
		session.close();
	}

	/** {@inheritDoc} */
	@Override
	public void setPermissionObjectID(String id) {
		PermissionProviderImplementation.getThreadlocalentityid().set(id);

	}
}